package org.xx.armory.db.junit;

import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.db.DbContext;
import org.xx.armory.db.SessionFactories;
import org.xx.armory.db.SessionFactory;
import org.xx.armory.db.TransactionProperties;
import org.xx.armory.db.impl.SimpleDataSource;

import javax.sql.DataSource;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import java.util.Date;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.DateTimeUtils.createLocalDate;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 数据库测试套件。
 *
 * <p>
 * 用于针对JUNIT编写单元测试, 增加了对数据库的访问功能。
 * </p>
 *
 * @author Haart
 */
public abstract class DbContextTestFixture
        implements DbContext {
    @ClassRule
    public static final DataSourceRef DATA_SOURCE = initDataSourceRef();
    private final String providerName;
    private final Logger logger = LoggerFactory.getLogger(DbContextTestFixture.class);
    private SessionFactory sessionFactory;

    /**
     * 构造数据库测试套件。
     * <p>构造时需要通过参数{@code providerName}指定使用的数据库。{@literal derby}表示</p>
     *
     * @param providerName
     *         会话工厂的提供程序名。
     */
    protected DbContextTestFixture(String providerName) {
        this.providerName = notBlank(providerName, "providerName").trim();
    }

    private static DataSourceRef initDataSourceRef() {
        return new DataSourceRef();
    }

    protected static DataSource getDataSource() {
        return DATA_SOURCE.get();
    }

    /**
     * 将查询结果转化为字符串。
     *
     * @param obj
     *         待转化的查询结果。
     * @return 字符串形式的结果，如果参数{@code obj}是{@code null}则返回空字符串。
     */
    protected static String toStr(
            Object obj
    ) {
        return obj != null ? obj.toString() : "";
    }

    /**
     * 将查询结果转化为整数。
     *
     * @param obj
     *         待转化的查询结果。
     * @return 如果转化成功则返回结果。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}不是{@code java.lang.Number}也不是{@code java.lang.String}。
     * @throws NumberFormatException
     *         如果参数{@code obj}是字符串, 但是无法被解析为整数。
     */
    protected static long toInt(
            Object obj
    ) {
        notNull(obj, "obj");

        if (obj instanceof Number) {
            return ((Number) obj).intValue();
        } else if (obj instanceof String) {
            return Integer.parseInt((String) obj);
        } else {
            throw new IllegalArgumentException("cannot cast " + obj + " to int");
        }
    }

    /**
     * 将查询结果转化为长整数。
     *
     * @param obj
     *         待转化的查询结果。
     * @return 如果转化成功则返回结果。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}不是{@code java.lang.Number}也不是{@code java.lang.String}。
     * @throws NumberFormatException
     *         如果参数{@code obj}是字符串, 但是无法被解析为长整数。
     */
    protected static long toLong(
            Object obj
    ) {
        notNull(obj, "obj");

        if (obj instanceof Number) {
            return ((Number) obj).longValue();
        } else if (obj instanceof String) {
            return Long.parseLong((String) obj);
        } else {
            throw new IllegalArgumentException("cannot cast " + obj + " to long");
        }
    }

    /**
     * 将查询结果转化为日期。
     *
     * @param obj
     *         待转化的查询结果。
     * @return 如果转化成功则返回结果。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}不是{@code java.util.Date}。
     */
    protected static Date toDate(
            Object obj
    ) {
        notNull(obj, "obj");

        if (obj instanceof Date) {
            return (Date) obj;
        } else {
            throw new IllegalArgumentException("cannot cast " + obj + " to java.util.Date");
        }
    }

    /**
     * 获取不包含毫秒部分的当前时间。
     *
     * @return 不包含毫秒部分的当前时间。
     */
    protected static Date currentDate() {
        final Date now = new Date();
        // 去掉毫秒部分。
        return new Date(now.getTime() / 1000 * 1000);
    }

    /**
     * 创建指定的日期。时间部分都是0。
     *
     * @param year
     *         年。
     * @param month
     *         月（从1开始）。
     * @param day
     *         日。
     * @return 指定的日期对象。
     * @throws IllegalArgumentException
     *         如果指定的年月日不表示合法的日期。
     */
    protected static Date time(
            int year,
            int month,
            int day
    ) {
        return time(year, month, day, 0, 0, 0);
    }

    /**
     * 创建指定的日期，包括日期和时间部分。
     *
     * @param year
     *         年。
     * @param month
     *         月（从1开始）。
     * @param day
     *         日。
     * @param hour
     *         时（24小时制）。
     * @param minute
     *         分。
     * @param second
     *         秒。
     * @return 指定的日期对象。
     * @throws IllegalArgumentException
     *         如果指定的年月日时分秒不表示合法的日期。
     */
    protected static Date time(
            int year,
            int month,
            int day,
            int hour,
            int minute,
            int second
    ) {
        return createLocalDate(year, month, day, hour, minute, second).getTime();
    }

    protected static BigDecimal decimal(
            Number n
    ) {
        return n != null ? BigDecimal.valueOf(n.longValue()) : null;
    }

    protected static BigDecimal decimal(
            String s
    ) {
        s = trimToEmpty(s);
        return !s.isEmpty() ? new BigDecimal(s) : null;
    }

    /**
     * 执行数据库相关的测试准备。
     *
     * @throws Exception
     *         如果初始化数据库时出现错误。
     */
    @Before
    public final void setUp()
            throws Exception {
        logger.info("initializing test fixture \"{}\"", this.getClass().getSimpleName());

        // 初始化会话工厂。
        final SessionFactory sessionFactory = SessionFactories.createFactory(this.providerName);
        sessionFactory.setDataSource(getDataSource());
        sessionFactory.getTransactionManager().setProperties(new TransactionProperties(true, false, false));
        sessionFactory.initialize();
        setSessionFactory(sessionFactory);

        // 执行额外的初始化。
        doSetUp();

        logger.info("initialized test fixture \"{}\"", this.getClass().getSimpleName());
    }

    /**
     * 执行数据库相关的清理。
     *
     * @throws Exception
     *         如果清理数据库时出现错误。
     */
    @After
    public final void tearDown()
            throws Exception {
        logger.info("destroying test fixture \"{}\"", this.getClass().getSimpleName());

        // 执行额外的清理。
        doTearDown();

        // 关闭会话工厂。
        this.sessionFactory.close();

        logger.info("destroyed test fixture \"{}\"", this.getClass().getSimpleName());
    }

    /**
     * 执行额外的初始化，此时{@link #getDataSource()}可用。
     *
     * @throws Exception
     *         如果执行额外初始化时出现错误。
     */
    protected void doSetUp()
            throws Exception {
    }

    /**
     * 执行额外的清理，此时{@link #getDataSource()}可用。
     *
     * @throws Exception
     *         如果执行额外清理时出现错误。
     */
    protected void doTearDown()
            throws Exception {
    }

    /**
     * 创建指定类型的{@code DbContext}实例。
     *
     * @param <T>
     *         要创建的{@code DbContext}实例的类型。
     * @param clazz
     *         要创建的{@code DbContext}实例的类型。
     * @return 创建的{@code DbContext}实例。
     * @throws IllegalArgumentException
     *         如果参数{@code class}是{@code null}。
     */
    protected final <T extends DbContext> T get(Class<T> clazz) {
        notNull(clazz, "clazz");

        try {
            T ret = clazz.getConstructor().newInstance();
            ret.setSessionFactory(getSessionFactory());
            return ret;
        } catch (ReflectiveOperationException ex) {
            throw new IllegalStateException("cannot create " + clazz, ex);
        }
    }

    /**
     * 获取用于访问数据库的会话工厂。
     *
     * @return 用于访问数据库的会话工厂。
     */
    public final SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * 设置用于访问数据库的会话工厂。
     *
     * @param sessionFactory
     *         用于访问数据库的会话工厂。
     * @throws IllegalArgumentException
     *         如果参数{@code sessionFactory}是{@code null}。
     * @throws IllegalStateException
     *         如果当前会话工厂已存在。
     */
    public final void setSessionFactory(SessionFactory sessionFactory) {
        notNull(sessionFactory, "sessionFactory");

        if (this.sessionFactory != null) {
            throw new IllegalStateException("current session factory already exists");
        }

        this.sessionFactory = sessionFactory;
    }

    /**
     * 用于管理数据源的参数。
     * <p>此注解可以位于基类测试用例中，所有的派生测试用例都可以使用基类中指定的测试配置。</p>
     */
    @Retention(value = RUNTIME)
    @Target(value = TYPE)
    @Inherited
    public @interface DataSourceArguments {
        /**
         * 获取访问数据库的驱动程序类型。
         *
         * @return 数据库的驱动程序类型。
         */
        String driverClass();

        /**
         * 获取访问数据库的URL。
         *
         * @return 访问数据库的URL。
         */
        String url();

        /**
         * 获取访问数据库的用户名。
         *
         * @return 访问数据库的用户名。
         */
        String userName() default "";

        /**
         * 获取访问数据库的口令。
         *
         * @return 访问数据库的口令。
         */
        String password() default "";
    }

    /**
     * 用于管理数据源的测试规则。
     */
    private static class DataSourceRef
            implements TestRule {
        private final Logger logger = LoggerFactory.getLogger(DbContextTestFixture.class);
        private SimpleDataSource value;

        public final DataSource get() {
            return this.value;
        }

        private void initDataSource() {
            try {
                this.value.initialize();
            } catch (Exception ex) {
                logger.error("cannot initialize data source", ex);
            }
        }

        private void destroyDataSource() {
            try {
                this.value.dispose();
            } catch (Exception ex) {
                logger.error("cannot dispose data source", ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Statement apply(
                Statement statement,
                Description description
        ) {
            notNull(statement, "statement");
            notNull(description, "description");

            final Class<?> clazz = description.getTestClass();
            notNull(clazz, "description.testClass");

            if (!DbContextTestFixture.class.isAssignableFrom(clazz)) {
                throw new IllegalArgumentException(clazz.getName() + " cannot convert to " + DbContextTestFixture.class.getName());
            }

            final DataSourceArguments arg = description.getAnnotation(DataSourceArguments.class);
            if (arg == null) {
                throw new IllegalStateException("class " + clazz + " has not annotation " + DataSourceArguments.class.getName());
            }

            final SimpleDataSource dataSource = new SimpleDataSource();
            dataSource.setDriverClassName(arg.driverClass());
            dataSource.setUrl(arg.url());
            dataSource.setUsername(arg.userName());
            dataSource.setPassword(arg.password());

            this.value = dataSource;

            return new Statement() {
                @Override
                public void evaluate()
                        throws Throwable {
                    initDataSource();

                    try {
                        statement.evaluate();
                    } finally {
                        destroyDataSource();
                    }
                }
            };
        }
    }

}
